<template>
  <div class="bg" :style="bgStyle"></div>
  <div class="wrap" :style="wrapStyle" @wheel.passive="onWheelSwitch">
    <SearchBar />

    <Transition name="group-switch" mode="out-in">
      <div class="grid-wrap" :key="activeGroupId">
        <BookmarkGrid :bookmarks="currentBookmarks" :groupId="activeGroupId" @edit="startEdit">
          <AddCard @add-widget="onAddWidget" @add-bookmark="onAddBookmark" />
        </BookmarkGrid>
      </div>
    </Transition>

    <!-- 左下角：主题切换 + 设置按钮 -->
    <div class="btns-fixed" v-if="!settingsOpen">
      <!-- <button type="button" class="theme" @click="toggleTheme" :title="`切换主题：当前${settingsStore.theme==='dark'?'深色':'浅色'}`">{{ settingsStore.theme==='dark' ? '🌙' : '☀️' }}</button> -->
      <button type="button" class="avatar" @click="openSettings" title="设置">⚙️</button>
    </div>
    
    <SettingsDrawer :open="settingsOpen" @close="settingsOpen=false" />

    <!-- 添加图标表单 -->
    <teleport to="body">
      <div v-if="showAdd" class="add-mask" @click="closeAdd"></div>
      <div v-if="showAdd" class="add-panel" @click.stop role="dialog" aria-modal="true" aria-labelledby="add-dialog-title">
        <header class="add-hdr">
          <button class="back" @click="closeAdd">返回</button>
          <strong id="add-dialog-title">自定义添加</strong>
          <button class="done" @click="submitAdd">完成</button>
        </header>
        <div class="form">
          <label class="field">
            <span>链接</span>
            <div class="row">
              <input ref="urlInput" v-model="form.url" placeholder="https://" @paste.prevent="onUrlPaste" @blur="onUrlBlur" class="flex1" />
              <button type="button" class="btn outline" @click="manualFetch" :disabled="fetchingIcon">{{ fetchingIcon ? '获取中…' : '获取图标' }}</button>
            </div>
          </label>
          <label class="field">
            <span>名称</span>
            <input v-model="form.title" placeholder="站点名称" @input="titleTouched=true" />
          </label>
          <label class="field">
            <span>所属分组</span>
            <select v-model="form.groupId" class="nice-select">
              <option v-for="g in bm.groups" :key="g.id" :value="g.id">{{ g.name }}</option>
            </select>
          </label>
          <div class="tabs">
            <button :class="{on: tab==='online'}" @click="setTab('online')">在线图标</button>
            <button :class="{on: tab==='solid'}" @click="setTab('solid')">纯色图标</button>
            <button :class="{on: tab==='local'}" @click="setTab('local')">本地上传</button>
          </div>
          <!-- 预览与交互区域 -->
          <div class="icon-area">
            <template v-if="tab==='online'">
              <div v-if="form.icon && iconSource==='online'" class="preview img"><img :src="form.icon" alt="预览" /></div>
              <div v-else class="preview" :style="{background: '#2b2f3c', color:'#fff'}">{{ previewLetter }}</div>
              <p class="tip">若成功获取到网站图标将直接展示；否则使用首字母占位（不强制联网）。</p>
            </template>
            <template v-else-if="tab==='solid'">
              <div class="preview" :style="{background: form.color}">{{ previewLetter }}</div>
              <div class="palette">
                <button v-for="c in presetColors" :key="c" type="button" class="dot" :style="{background:c}" @click="pickColor(c)" :title="c"></button>
                <label class="dot custom" title="自定义颜色">
                  <input type="color" @input="(e:any)=>pickColor(e.target.value)" />
                </label>
              </div>
            </template>
            <template v-else>
              <div class="row tight">
                <button type="button" class="btn outline" @click="fileInput?.click()">选择文件</button>
                <input ref="fileInput" type="file" accept="image/*" @change="onPickLocal" style="display:none" />
                <span class="filename">{{ iconFileName || '未选择任何文件' }}</span>
              </div>
              <div v-if="form.icon && iconSource==='local'" class="preview img">
                <img :src="form.icon" alt="预览" />
              </div>
            </template>
          </div>
          <p class="tip">默认不联网获取网站图标。选择“在线图标”将使用名称首字母占位；你也可以改为“纯色图标”或“本地上传”。</p>
        </div>
      </div>
    </teleport>
    <!-- 右侧分组侧栏 -->
    <GroupSidebar v-model="activeGroupId" />
  </div>
  <!-- 底部 Dock 快捷栏（固定定位，传送到 body 保证层级与可见性） -->
  <teleport to="body">
    <DockBar v-if="readyForDock" />
  </teleport>
</template>

<script setup lang="ts">
import SearchBar from './components/SearchBar.vue';
import BookmarkGrid from './components/BookmarkGrid.vue';
import AddCard from './components/AddCard.vue';
import SettingsDrawer from './components/SettingsDrawer.vue';
import GroupSidebar from './components/GroupSidebar.vue';
import DockBar from './components/DockBar.vue';
import { useBookmarks } from '../core/store/useBookmarks';
import { useSettings } from '../core/store/useSettings';
import { onMounted, onUnmounted, reactive, ref, nextTick, computed, watch } from 'vue';
import { isHttpUrl } from '../core/utils/validators';
// 引入 Iconfont 的 symbol 注入脚本（离线）
import './assets/iconfont/iconfont.js';
// 统一反馈
import { toast } from '../core/utils/feedback';
// 复用通用工具
import { fetchWithTimeout } from '../core/utils/net';
import { tryFetchFavicon } from '../core/utils/favicon';
import { readSiteCache, writeSiteCache } from '../core/utils/siteCache';
import { SITE_CACHE_KEY, SITE_CACHE_TTL_MS, FAVICON_TIMEOUT_MS } from '../core/config';

// 常见站点标题映射（用于 CORS 无法读取 <title> 的兜底）
const HOST_TITLE_MAP: Array<{ test: (host: string) => boolean; title: string }> = [
  { test: (h) => /(^|\.)baidu\.com$/i.test(h), title: '百度一下，你就知道' },
  { test: (h) => /(^|\.)google\.[a-z.]+$/i.test(h), title: 'Google' },
  { test: (h) => /(^|\.)163\.com$/i.test(h), title: '网易' },
];
function fallbackTitleByHost(host: string): string | '' {
  const hit = HOST_TITLE_MAP.find(x => x.test(host));
  return hit ? hit.title : '';
}

const bm = useBookmarks();
const settingsStore = useSettings();
const readyForDock = ref(false);
const cleanupTasks: Array<() => void> = [];

async function manualFetch(){
  try{
    // 若用户尚未输入名称，则先用域名推导一个占位
    if(!form.title && form.url && /^https?:\/\//i.test(form.url)){
      try{ form.title = deriveNameFromHost(new URL(form.url).hostname); autoTitleFromHost.value = true; }catch{}
    }
    await fetchIconAndTitle({ silent: false });
  }catch{}
}

let lastFetchUrl = '';
async function fetchIconAndTitle(opts: { silent?: boolean } = {}){
  const silent = !!opts.silent;
  const u = (form.url || '').trim();
  if(!/^https?:\/\//i.test(u)) { if(!silent) try{ toast('请输入有效的 http(s) 链接'); }catch{}; return; }
  if(fetchingIcon.value) return;
  if (u === lastFetchUrl && form.icon) return; // 避免同一URL重复抓取
  fetchingIcon.value = true;
  try{
    const target = new URL(u);
    lastFetchUrl = u;
    // 0) 本地缓存（host 维度）
    const host = target.host;
    const cacheKey = SITE_CACHE_KEY;
    const cacheMap = await readSiteCache(cacheKey);
    const now = Date.now();
    const hit = cacheMap[host] && (now - cacheMap[host].ts < SITE_CACHE_TTL_MS);
    if(hit){
      const c = cacheMap[host];
      if(c.title && (autoTitleFromHost.value || !form.title)){
        // 若缓存的标题只是域名派生占位，则尝试使用映射更友好的标题
        const derived = deriveNameFromHost(host);
        if (c.title.trim().toLowerCase() === derived.trim().toLowerCase()) {
          const fb = fallbackTitleByHost(host);
          if (fb) { form.title = fb; autoTitleFromHost.value = false; }
          else { form.title = c.title; autoTitleFromHost.value = false; }
        } else {
          form.title = c.title; autoTitleFromHost.value = false;
        }
      } else if (autoTitleFromHost.value || !form.title) {
        const fb = fallbackTitleByHost(host);
        if (fb) { form.title = fb; autoTitleFromHost.value = false; }
      }
      if(c.icon){ form.icon = c.icon; tab.value = 'online'; iconSource.value = 'online'; }
      fetchingIcon.value = false;
      return;
    }
    // 1) 先尝试直接 /favicon.ico
    let iconDataUrl = await tryFetchFavicon(new URL('/favicon.ico', target.origin).toString());
    // 2) 如失败，抓取HTML解析 <link rel="icon"> 与 <title>
    let pageTitle = '';
    if(!iconDataUrl){
      try{
        const htmlRes = await fetchWithTimeout(target.origin, { mode: 'cors' }, FAVICON_TIMEOUT_MS);
        if(htmlRes.ok){
          const html = await htmlRes.text();
          const dom = new DOMParser().parseFromString(html, 'text/html');
          // title
          const t = dom.querySelector('title')?.textContent?.trim();
          if(t && !form.title){ pageTitle = t; }
          // icon candidates
          const iconLinks = Array.from(dom.querySelectorAll('link[rel~="icon" i], link[rel="shortcut icon" i]')) as HTMLLinkElement[];
          for(const lk of iconLinks){
            const href = lk.getAttribute('href') || '';
            if(!href) continue;
            const abs = new URL(href, target.origin).toString();
            iconDataUrl = await tryFetchFavicon(abs);
            if(iconDataUrl) break;
          }
        }
      }catch{}
    }
    // 3) 应用结果
    if(pageTitle && (autoTitleFromHost.value || !form.title)){ form.title = pageTitle; autoTitleFromHost.value = false; }
    else if (autoTitleFromHost.value || !form.title) {
      const fb = fallbackTitleByHost(host);
      if (fb) { form.title = fb; autoTitleFromHost.value = false; }
    }
    if(iconDataUrl){ form.icon = iconDataUrl; tab.value = 'online'; iconSource.value = 'online'; }
    else if(!silent){ try{ toast('未获取到图标，已保留首字母/纯色/本地上传'); }catch{} }
    // 4) 写入缓存（优先缓存更友好标题）
    const titleToCache = pageTitle || fallbackTitleByHost(host) || form.title || '';
    cacheMap[host] = { title: titleToCache, icon: iconDataUrl || form.icon || '', ts: now };
    await writeSiteCache(cacheKey, cacheMap);
    if(!silent && (pageTitle || iconDataUrl)){
      try{ toast('已获取站点信息'); }catch{}
    }
  }catch(e){
    if(!silent){ try{ toast('获取失败'); }catch{} }
  }finally{
    fetchingIcon.value = false;
  }
}
onMounted(async ()=>{ 
  // 确保设置已初始化
  try { if (!(settingsStore as any).inited && typeof (settingsStore as any).init === 'function') { await (settingsStore as any).init(); } } catch {}
  // 确保书签与分组已初始化
  try { if (!(bm as any).inited && typeof (bm as any).init === 'function') { await (bm as any).init(); } } catch {}
  applyTheme(settingsStore.theme);
  ensureActiveGroup();
  // Dock 就绪标记，避免刷新后 Dock 在依赖未加载时短暂缺失
  readyForDock.value = true;
  // 全局确认/Toast 由 main.ts 注入（feedback.ts），此处不再重复注入

  // 页面隐藏/关闭前，强制持久化，避免拖拽等操作的防抖未写入
  const onBeforeUnload = async () => {
    try { await bm.persistNow?.(); } catch {}
  };
  const onVisibility = async () => {
    if (document.visibilityState === 'hidden') {
      try { await bm.persistNow?.(); } catch {}
    }
  };
  window.addEventListener('beforeunload', onBeforeUnload);
  cleanupTasks.push(() => window.removeEventListener('beforeunload', onBeforeUnload));
  document.addEventListener('visibilitychange', onVisibility);
  cleanupTasks.push(() => document.removeEventListener('visibilitychange', onVisibility));
  // ESC 关闭添加模态框
  const onKeydownEsc = (e: KeyboardEvent) => {
    if (e.key === 'Escape' && showAdd.value) { e.preventDefault(); closeAdd(); }
  };
  document.addEventListener('keydown', onKeydownEsc);
  cleanupTasks.push(() => document.removeEventListener('keydown', onKeydownEsc));

  // 监听后台消息，书签更新后刷新
  const rt = (globalThis as any).chrome?.runtime;
  if (rt?.onMessage?.addListener) {
    const handleMsg = (message: any, _sender: any, sendResponse?: (p: any) => void) => {
      if (message?.type === 'GCE_BOOKMARKS_UPDATED') {
        Promise.resolve(bm.reload?.()).catch(() => {});
        if (typeof sendResponse === 'function') sendResponse({ ok: true });
      }
    };
    rt.onMessage.addListener(handleMsg);
    cleanupTasks.push(() => rt.onMessage.removeListener(handleMsg));
  }

  // 监听存储变更（localGce），确保跨页同步，但做防抖与字段过滤，避免连续空-有-空导致图标闪烁
  const st = (globalThis as any).chrome?.storage;
  if (st?.onChanged?.addListener) {
    let reloadTimer: any = null;
    const handleChange = (changes: Record<string, any>, areaName: string) => {
      if (areaName !== 'local' || !changes?.localGce) return;
      // 仅当书签或设置发生变化时再触发重载
      const nv = changes.localGce.newValue || {};
      const ov = changes.localGce.oldValue || {};
      const bookmarksChanged = JSON.stringify(nv.bookmarks || []) !== JSON.stringify(ov.bookmarks || []);
      const settingsChanged = JSON.stringify(nv.settings || {}) !== JSON.stringify(ov.settings || {});
      if (!bookmarksChanged && !settingsChanged) return;
      // 合并多次事件，200ms 内只 reload 一次
      if (reloadTimer) clearTimeout(reloadTimer);
      reloadTimer = setTimeout(async () => {
        try{ await bm.reload?.(); ensureActiveGroup(); }catch{}
      }, 200);
    };
    st.onChanged.addListener(handleChange);
    cleanupTasks.push(() => st.onChanged.removeListener(handleChange));
  }
  // 记录清理函数
  (window as any).__gce_cleanup = () => {
    try {
      while (cleanupTasks.length) {
        const fn = cleanupTasks.pop();
        try { fn?.(); } catch {}
      }
    } catch {}
  };
});

onUnmounted(()=>{ try{ (window as any).__gce_cleanup?.(); }catch{} });

const settingsOpen = ref(false);
const showAdd = ref(false);
const tab = ref<'online'|'solid'|'local'>('online');
const iconSource = ref<'online'|'local'|''>('');
const autoTitleFromHost = ref(false);
const titleTouched = ref(false);
const form = reactive<{ id?: string; url: string; title: string; icon?: string; color?: string; groupId: string }>({ id: undefined, url: 'https://', title: '', icon: '', color: '#3b82f6', groupId: 'default' });
const urlInput = ref<HTMLInputElement | null>(null);
const fileInput = ref<HTMLInputElement | null>(null);
const iconFileName = ref('');
const fetchingIcon = ref(false);
const activeGroupId = ref<string>('default');
const currentBookmarks = computed(()=>{
  const list = Array.isArray(bm.bookmarks) ? bm.bookmarks : [];
  return list
    .filter(b => (b.groupId ?? 'default') === activeGroupId.value)
    .slice()
    .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
});
const wrapStyle = computed(()=> ({ 
  maxWidth: (settingsStore.settings?.contentMaxWidth || 1440) + 'px',
  background: settingsStore.settings?.backgroundDataUrl ? 'transparent' : undefined
}));
const bgStyle = computed(()=>{
  const s = settingsStore.settings || ({} as any);
  if(!s.backgroundDataUrl) return { display:'none' } as any;
  const ov = Math.min(0.6, Math.max(0, Number(s.backgroundOverlay ?? 0)));
  const bl = Math.min(20, Math.max(0, Number(s.backgroundBlur ?? 0)));
  const pos = s.backgroundPosition || 'center';
  const size = s.backgroundSize || 'cover';
  const repeat = size === 'tile' ? 'repeat' : 'no-repeat';
  const cssSize = size === 'tile' ? 'auto' : size;
  return {
    backgroundImage: `linear-gradient(rgba(0,0,0,${ov}), rgba(0,0,0,${ov})), url('${s.backgroundDataUrl}')`,
    backgroundPosition: pos,
    backgroundSize: cssSize,
    backgroundRepeat: repeat,
    filter: `blur(${bl}px)`
  } as any;
});
const presetColors = ['#f97316','#f43f5e','#ef4444','#eab308','#22c55e','#10b981','#06b6d4','#0ea5e9','#3b82f6','#6366f1','#a855f7','#ec4899','#94a3b8','#475569','#0f172a'];
function pickColor(c: string){ form.color = c; }
function deriveNameFromHost(host: string): string{
  try{
    const h = (host || '').toLowerCase().replace(/^www\./, '');
    const parts = h.split('.').filter(Boolean);
    if (parts.length <= 1) return h || '站点';
    // 保留二级域（去掉常见顶级域）
    const tld = parts[parts.length - 1];
    const name = parts[parts.length - 2];
    // 对于多级如 news.163.com，优先数字域或更有辨识度的倒数第二段
    return name || h;
  }catch{ return host || '站点'; }
}
const previewLetter = computed(()=>{
  const t = (form.title || '').trim();
  if (t) return t[0].toUpperCase();
  try{
    const u = (form.url || '').trim();
    if (/^https?:\/\/[^/]+/i.test(u)) {
      const host = new URL(u).hostname || '';
      return (host[0] || '站').toUpperCase();
    }
  }catch(e){/* ignore */}
  return '站';
});

function startEdit(bmItem: any){
  form.id = bmItem.id;
  form.url = bmItem.url;
  form.title = bmItem.title;
  form.icon = bmItem.icon || '';
  form.color = bmItem.color || '#3b82f6';
  form.groupId = bmItem.groupId || 'default';
  // 推断图标 tab
  if(form.icon){ tab.value = 'online'; iconSource.value = 'online'; }
  else if(form.color){ tab.value = 'solid'; }
  else { tab.value = 'online'; }
  showAdd.value = true;
  nextTick(() => urlInput.value?.focus());
}

function onAddWidget(){
  try{ (window as any).wetabToast?.('小组件暂未实现，后续支持'); }catch{}
}
function onAddBookmark(){
  form.url = 'https://';
  form.title = '';
  form.icon = '';
  form.color = '#3b82f6';
  form.groupId = activeGroupId.value;
  form.id = undefined;
  tab.value = 'online'; iconSource.value = '';
  showAdd.value = true;
  nextTick(() => urlInput.value?.focus());
}
function closeAdd(){ showAdd.value = false; }
function openSettings(){
  settingsOpen.value = true;
}
function applyTheme(th: string){
  const el = document.documentElement;
  el.setAttribute('data-theme', th as any);
}
function toggleTheme(){
  const next = settingsStore.theme === 'dark' ? 'light' : 'dark';
  settingsStore.setTheme?.(next);
  applyTheme(next);
}
// 鼠标滚轮切换分组（在主区域滚动时）
let lastWheelAt = 0;
function onWheelSwitch(e: WheelEvent){
  // 仅纵向滚动且不按住修饰键时触发
  if (Math.abs(e.deltaY) < Math.abs(e.deltaX)) return;
  if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
  const now = Date.now();
  if (now - lastWheelAt < 500) return; // 增加节流时间，减少意外触发
  lastWheelAt = now;
  const list = (bm.groups ?? []).slice().sort((a,b)=> (a.order??0)-(b.order??0));
  if (!list.length) return;
  const idx = Math.max(0, list.findIndex(g=> g.id === activeGroupId.value));
  const next = e.deltaY > 0 ? (idx + 1) % list.length : (idx - 1 + list.length) % list.length;
  activeGroupId.value = list[next].id;
}
async function submitAdd(){
  if(!form.url || !isHttpUrl(form.url)){
    toast('请输入以 http(s):// 开头的链接');
    return;
  }
  if(!form.title){
    try{ form.title = new URL(form.url).hostname; }catch{ /* 保底 */ }
  }
  // 根据当前 tab 组合图标信息（不做外网请求）
  let icon = form.icon || '';
  let color = form.color || undefined;
  if(tab.value==='online' && !icon){
    // 生成首字母占位，不联网
    icon = '';
  }
  if(tab.value!=='solid') color = undefined;
  if(form.id){
    // 编辑：更新基础信息
    await bm.updateBookmark(form.id, { title: form.title, url: form.url, icon, color });
    // 分组变化时移动
    const cur = bm.bookmarks.find(b => b.id === form.id);
    if(cur && (cur.groupId ?? 'default') !== form.groupId){
      await bm.moveToGroup(form.id, form.groupId);
    }
  }else{
    // 新增
    await bm.addBookmark({ title: form.title, url: form.url, icon, color, groupId: form.groupId || activeGroupId.value });
  }
  form.url=''; form.title=''; form.icon=''; form.color='#3b82f6'; showAdd.value=false;
  if(fileInput.value) fileInput.value.value = '';
}

function onPickLocal(e: Event){
  const f = (e.target as HTMLInputElement).files?.[0];
  if(!f) return;
  iconFileName.value = f.name || '';
  const reader = new FileReader();
  reader.onload = () => { form.icon = String(reader.result || ''); iconSource.value = 'local'; };
  reader.readAsDataURL(f);
}

async function onUrlBlur(){
  const u = (form.url || '').trim();
  // 仅校验格式，不进行任何联网图标推断，保持纯离线
  if(!/^https?:\/\//i.test(u)) return;
  // 若名称为空，则用主机名填充，提升可用性（离线）
  if(!form.title){
    try{ const host = new URL(u).hostname; form.title = deriveNameFromHost(host); autoTitleFromHost.value = true; }catch{}
  }
  // 自动尝试联网获取图标与标题（静默失败）
  try{ await fetchIconAndTitle({ silent: true }); }catch{}
}

function setTab(t: 'online'|'solid'|'local'){
  tab.value = t;
  // 仅切换显示来源，不改变已获取的 form.icon；预览由 iconSource 控制
}
function onUrlPaste(e: ClipboardEvent){
  const raw = e.clipboardData?.getData('text') || '';
  const t = raw.trim();
  if(!t) return;
  // 若包含协议，直接使用粘贴值；否则补上 https://
  const hasProto = /^https?:\/\//i.test(t);
  form.url = hasProto ? t : `https://${t.replace(/^\/*/, '')}`;
  // 立刻用域名推导名称（仅当用户未手动输入过名称）
  if(!titleTouched.value && !form.title){
    try{ const host = new URL(form.url).hostname; form.title = deriveNameFromHost(host); autoTitleFromHost.value = true; }catch{}
  }
  nextTick(()=> urlInput.value?.setSelectionRange(form.url.length, form.url.length));
}

// 侧栏自身带有“新增分类”按钮，无需在 App 再提供

// 当分组列表变化时，若当前分组不存在则回退到第一个（立即执行）
watch(
  () => (Array.isArray(bm.groups) ? bm.groups.map(g=>g.id).join(',') : ''),
  () => ensureActiveGroup(),
  { immediate: true }
);

watch(() => settingsStore.theme, (v)=> applyTheme(v as any));

// URL 实时监听：在用户未修改名称的情况下，用域名推导名称占位
watch(() => form.url, (u) => {
  const t = (u || '').trim();
  if(!/^https?:\/\//i.test(t)) return;
  if(titleTouched.value) return; // 用户手改过名称则不再覆盖
  if(!form.title || autoTitleFromHost.value){
    try{ const host = new URL(t).hostname; form.title = deriveNameFromHost(host); autoTitleFromHost.value = true; }catch{}
  }
});

function ensureActiveGroup(){
  const list = Array.isArray(bm.groups) ? bm.groups : [];
  if (!list.length) { activeGroupId.value = 'default'; return; }
  const hit = list.find(g => g.id === activeGroupId.value);
  if (!hit) activeGroupId.value = list[0].id;
}
</script>

<style scoped>
.bg{ position:fixed; inset:0; background-position: center; background-size: cover; background-repeat:no-repeat; z-index:0; transform: translateZ(0) }
.wrap { position: relative; z-index: 1; min-height: 100%; padding: 24px; padding-bottom: 84px; box-sizing: border-box; max-width: 1440px; margin: 0 auto; padding-right: 120px; padding-left: 120px; }
@media (max-width: 1100px){
  .wrap{ padding-right: 132px; padding-left: 132px; }
}
@media (max-width: 860px){
  .wrap{ padding-right: 144px; padding-left: 144px; }
}
.btns-fixed{ position:fixed; left:16px; bottom:16px; z-index:100000; display:flex; flex-direction:column; gap:10px }
.avatar{ width:44px; height:44px; border-radius:50%; border:1px solid var(--border); background:var(--glass-dark); backdrop-filter: blur(6px); color:var(--text); cursor:pointer; box-shadow: 0 8px 18px rgba(0,0,0,.25); transition: transform .15s ease }
.theme{ width:44px; height:44px; border-radius:50%; border:1px solid var(--border); background:var(--glass-dark); backdrop-filter: blur(6px); color:var(--text); cursor:pointer; box-shadow: 0 8px 18px rgba(0,0,0,.25); }
.avatar:hover{ transform: translateY(-1px) }
.add-mask{ position:fixed; inset:0; background:rgba(0,0,0,.35); z-index:200011 }
.add-panel{ position:fixed; left:50%; top:50%; transform: translate(-50%, -50%); width:min(560px, 92vw); max-height:80vh; background:var(--glass-dark); backdrop-filter: blur(6px); color:var(--text); border:1px solid var(--border); box-shadow: 0 18px 48px rgba(0,0,0,.45); padding:16px; z-index:200021; border-radius:14px; overflow:auto }
.add-hdr{ display:flex; align-items:center; justify-content:space-between; margin-bottom:12px }
.back, .done{ border:1px solid var(--border); background:var(--card); color:var(--text); border-radius:8px; padding:6px 10px; cursor:pointer }
.form{ display:flex; flex-direction:column; gap:12px }
.field{ display:flex; flex-direction:column; gap:6px }
.field input{ height:38px; border-radius:10px; border:1px solid var(--border); background:var(--card); color:var(--text); padding:0 12px }
.form .row{ display:flex; align-items:center; gap:12px }
.flex1{ flex:1; min-width:0 }
.tabs{ display:flex; gap:8px }
.tabs button{ border:1px solid var(--border); background:var(--card); color:var(--text); border-radius:10px; padding:6px 10px; cursor:pointer }
.tabs .on{ outline:2px solid var(--primary) }
.grid-wrap{ will-change: opacity, transform }
.group-switch-enter-active, .group-switch-leave-active{ transition: opacity .28s ease, transform .28s ease }
.group-switch-enter-from, .group-switch-leave-to{ opacity:0; transform: translateY(6px) }
.group-switch-enter-to, .group-switch-leave-from{ opacity:1; transform: translateY(0) }
.icon-area{ display:flex; flex-direction:column; gap:8px }
.icon-area .row{ display:flex; align-items:center; gap:8px }
.preview{ width:48px; height:48px; border-radius:12px; display:flex; align-items:center; justify-content:center; overflow:hidden }
.preview.img img{ width:100%; height:100%; object-fit:cover }
.tip{ opacity:.7; font-size:12px }
.nice-select{ height:38px; border-radius:10px; border:1px solid var(--border); background:var(--card); color:var(--text); padding:0 34px 0 12px; appearance:none; -webkit-appearance:none; background-image: linear-gradient(45deg, transparent 50%, var(--text) 50%), linear-gradient(135deg, var(--text) 50%, transparent 50%); background-position: calc(100% - 18px) 16px, calc(100% - 12px) 16px; background-size: 6px 6px, 6px 6px; background-repeat:no-repeat }
.nice-select:hover{ box-shadow: 0 6px 16px rgba(0,0,0,.18) }
.nice-select:focus{ outline:2px solid var(--primary); outline-offset:2px }
.nice-select option{ background: var(--card); color: var(--text) }

/* light 主题整体背景与层次 */
:host-context([data-theme='light']) .wrap{ background: #f3f4f6 }
.btn{ padding:8px 12px; border-radius:10px; cursor:pointer; border:1px solid var(--border); background:var(--card); color:var(--text) }
.btn.solid{ background: var(--primary); color:#fff; border-color: transparent }
.btn.outline{ background: var(--card) }
.palette{ display:flex; flex-wrap:wrap; gap:8px }
.palette .dot{ width:22px; height:22px; border-radius:50%; border:1px solid var(--border); cursor:pointer }
.palette .custom{ display:inline-flex; align-items:center; justify-content:center; background:conic-gradient(#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00) }
.palette .custom input{ opacity:0; width:22px; height:22px; cursor:pointer }
</style>
